package mylittleinvestigations;

import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import javax.imageio.ImageIO;
import mylittleinvestigations.CaseInformation.Case;
import mylittleinvestigations.CaseInformation.CaseParseException;
import mylittleinvestigations.CaseInformation.CaseXmlReader;
import mylittleinvestigations.CaseInformation.CaseXmlWriter;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;

public class Video
{
    private String id;
    private ArrayList<Video.Frame> frameList;
    private Video.Frame curFrame;
    private int curFrameIndex;
    private Path tempDirectoryPath;
    
    private ArrayList<String> locationParents = new ArrayList<String>();

    public Video(String id)
    {
        this.id = id;
        this.frameList = new ArrayList<Video.Frame>();
        this.curFrame = null;
        this.curFrameIndex = -1;
        this.shouldLoop = true;
    }
    
    private boolean shouldLoop;
    
    public boolean getShouldLoop() { return this.shouldLoop; }
    public void setShouldLoop(boolean shouldLoop) { this.shouldLoop = shouldLoop; }

    public void AddFrame(int msDuration, String filePath) throws IOException
    {
        this.frameList.add(new Video.Frame(this.id, this.frameList.size(), msDuration, filePath));
    }

    public void AddFrame(int msDuration, BufferedImage originalImage) throws IOException
    {
        Video.Frame frame = new Video.Frame(this.id, this.frameList.size(), msDuration, null);
        frame.SaveImage(this.GetTemporaryDirectoryPath(), originalImage);
        this.frameList.add(frame);
    }

    public void AddHoofstep()
    {
        this.frameList.get(this.frameList.size() - 1).AddHoofstep();
    }
        
    public void AddSpecifiedSound(String sfxId)
    {
        this.frameList.get(this.frameList.size() - 1).AddSpecifiedSound(sfxId);
    }

    public void Begin()
    {
        this.Reset();
        this.curFrame.Begin();
    }

    public void Update(GameContainer gc, int delta)
    {
        this.curFrame.Update(gc, delta);

        if (this.curFrame.getIsFinished())
        {
            double overflowDuration = this.curFrame.getOverflowDuration();
            this.curFrameIndex++;

            if (this.curFrameIndex == this.frameList.size())
            {
                this.curFrameIndex = 0;
            }

            this.curFrame = this.frameList.get(this.curFrameIndex);
            this.curFrame.Begin(overflowDuration);
        }
    }

    public void Draw(GameContainer gc, Graphics g, Vector2 position, boolean flipHorizontally, float scale)
    {
        this.curFrame.Draw(gc, g, position, flipHorizontally, scale);
    }

    public void Reset()
    {
        this.curFrameIndex = 0;
        this.curFrame = this.frameList.get(0);
    }

    public Video Clone()
    {
        Video cloneAnimation = new Video(this.id);

        cloneAnimation.frameList = this.frameList;
        cloneAnimation.curFrame = this.curFrame;
        cloneAnimation.curFrameIndex = this.curFrameIndex;
        cloneAnimation.shouldLoop = this.shouldLoop;

        return cloneAnimation;
    }
    
    public void EnsureTemporaryDirectoryPath() throws IOException
    {
        if (this.tempDirectoryPath == null)
        {
            this.tempDirectoryPath = Files.createTempDirectory("mli" + this.id);
        }
    }
    
    public String GetTemporaryDirectoryPath() throws IOException
    {
        this.EnsureTemporaryDirectoryPath();
        return this.tempDirectoryPath.toString();
    }

    public void SaveToXml(CaseXmlWriter writer) throws IOException, CaseParseException
    {
        writer.StartElement("Video");
        writer.WriteTextElement("Id", this.id);
        writer.WriteBooleanElement("ShouldLoop", this.shouldLoop);
        
        this.EnsureTemporaryDirectoryPath();
        
        for (int i = 0; i < this.frameList.size(); i++)
        {
            this.frameList.get(i).SaveToXml(writer, this.tempDirectoryPath.toString());
        }
        
        // We'll assume that all of the files have the same extension and resolution.
        String extension = this.frameList.get(0).getFilePath() == null ? ".png" : this.frameList.get(0).getFilePath().substring(this.frameList.get(0).getFilePath().lastIndexOf('.'));
        String originalFilePath = Paths.get(this.tempDirectoryPath.toString(), this.id + "Frame%05d" + extension).toString();
        String relativeFilePath = Paths.get("Videos", this.id + ".mov").toString();
        String finalFilePath = Paths.get(Case.getInstance().getFilePath(), relativeFilePath).toString();
            
        writer.WriteTextElement("VideoRelativeFilePath", relativeFilePath.replace('\\', '/'));
        writer.WriteIntElement("Width", this.frameList.get(0).getWidth());
        writer.WriteIntElement("Height", this.frameList.get(0).getHeight());
        
        try
        {
            Runtime rt = Runtime.getRuntime();
            
            Process p = rt.exec("ffmpeg.exe -i \"" + originalFilePath + "\" -codec:v qtrle \"" + finalFilePath + "\"");
            p.waitFor();
            
            p.destroy();
        }
        catch(Exception ex)
        {
            throw new CaseParseException(ex.getMessage());
        }
        
        writer.EndElement();

        for (String locationParent : this.locationParents)
        {
            Case.getInstance().AddParentLocationForVideoId(this.id, locationParent);
        }
        
        RecursivelyDeleteDirectory(this.tempDirectoryPath.toFile());
    }                               

    private static boolean RecursivelyDeleteDirectory(File fileOrDirectory)
    {
        if (fileOrDirectory.isDirectory())
        {
            String[] children = fileOrDirectory.list();
            for (int i = 0; i < children.length; i++)
            {
                RecursivelyDeleteDirectory(new File(fileOrDirectory, children[i]));
            }
        }

        // If we've reached this stage, then either this is a file,
        // or it's a directory that is empty.  Either way, we can delete it.
        return fileOrDirectory.delete();
    }

    public Video(CaseXmlReader reader)
    {
        this.frameList = new ArrayList<Video.Frame>();
        this.curFrame = null;
        this.curFrameIndex = -1;
        
        reader.StartElement("Video");
        
        reader.StartList("Frame");
        
        while (reader.MoveToNextListItem())
        {
            this.frameList.add(new Video.Frame(reader));
        }
        
        reader.EndElement();
    }
    
    public void markChildSprites(String markParent)
    {
        if (!this.locationParents.contains(markParent))
        {
            this.locationParents.add(markParent);
        }
    }

    private class Frame
    {
        private String animationId;
        private int frameIndex;
        private int msDuration;
        private String filePath;
        private Video.Frame.Sound sound;
        private int width;
        private int height;
        private boolean imageSaved;

        private double elapsedDuration;

        public double getOverflowDuration()
        {
            double overflowDuration = elapsedDuration - this.msDuration;

            if (overflowDuration > 0)
            {
                return overflowDuration;
            }
            else
            {
                return 0;
            }
        }

        public boolean getIsFinished()
        {
            return this.msDuration > 0 && this.elapsedDuration >= this.msDuration;
        }
        
        public int getMsDuration()
        {
            return this.msDuration;
        }
        
        public String getFilePath()
        {
            return this.filePath;
        }
        
        public int getWidth()
        {
            return width;
        }
        
        public int getHeight()
        {
            return height;
        }

        public Frame(String animationId, int frameIndex, int msDuration, String filePath)
        {
            this.animationId = animationId;
            this.frameIndex = frameIndex;
            this.msDuration = msDuration;
            this.filePath = filePath;
            this.imageSaved = false;
        }
        
        public void AddHoofstep()
        {
            this.sound = new Video.Frame.HoofstepSound();
        }
        
        public void AddSpecifiedSound(String sfxId)
        {
            this.sound = new Video.Frame.SpecifiedSound(sfxId);
        }

        public void Begin()
        {
            this.elapsedDuration = 0;
        }

        public void Begin(double overflowDuration)
        {
            this.elapsedDuration = overflowDuration;
        }

        public void Update(GameContainer gc, int delta)
        {
            this.elapsedDuration += delta;
        }

        public void Draw(GameContainer gc, Graphics g, Vector2 position, boolean flipHorizontally, float scale)
        {
            //this.getSprite().Draw(gc, g, position, Color.white, scale, flipHorizontally);
        }

        private void SaveToXml(CaseXmlWriter writer, String tempFolderPath) throws IOException
        {
            writer.StartElement("Frame");
            writer.WriteIntElement("MsDuration", this.msDuration);
            writer.WriteIntElement("FrameIndex", this.frameIndex);
            this.SaveImage(tempFolderPath);
            
            if (this.sound != null)
            {
                writer.StartElement("Sound");
                this.sound.SaveToXml(writer);
                writer.EndElement();
            }
            
            writer.EndElement();
        }
    
        private void SaveImage(String tempFolderPath) throws IOException
        {
            if (this.imageSaved)
            {
                return;
            }
            
            BufferedImage originalImage = ImageIO.read(Paths.get("data", this.filePath).toFile());
            
            this.SaveImage(tempFolderPath, originalImage);
        }
        
        public void SaveImage(String tempFolderPath, BufferedImage originalImage) throws IOException
        {
            if (this.imageSaved)
            {
                return;
            }
            
            String extension = this.filePath == null ? ".png" : this.filePath.substring(this.filePath.lastIndexOf('.'));
            String newFilePath = Paths.get(tempFolderPath, this.animationId + "Frame" + String.format("%05d", this.frameIndex) + extension).toString().replaceAll("\\\\","/");
            
            int paddedWidth = originalImage.getWidth() + (originalImage.getWidth() % 4 != 0 ? 4 - (originalImage.getWidth() % 4) : 0);
            int paddedHeight = originalImage.getHeight() + (originalImage.getHeight() % 4 != 0 ? 4 - (originalImage.getHeight() % 4) : 0);
            BufferedImage newImage = new BufferedImage(paddedWidth, paddedHeight, BufferedImage.TYPE_4BYTE_ABGR);
            
            Graphics2D newImageGraphics = newImage.createGraphics();
            newImageGraphics.setComposite(AlphaComposite.Clear);
            newImageGraphics.fillRect(0, 0, newImage.getWidth(), newImage.getHeight());
            newImageGraphics.setComposite(AlphaComposite.SrcOver);
            newImageGraphics.drawImage(originalImage, null, 0, 0);
            
            ImageIO.write(newImage, "png", new File(newFilePath));
            
            this.width = paddedWidth;
            this.height = paddedHeight;
            this.imageSaved = true;
            this.filePath = newFilePath;
        }

        public Frame(CaseXmlReader reader)
        {
            this.msDuration = reader.ReadIntElement("MsDuration");
            //this.filePath = reader.ReadTextElement("FilePath");
        }
        
        private abstract class Sound
        {
            public abstract void Play();
            public abstract void SaveToXml(CaseXmlWriter writer) throws IOException;
        }
        
        private class HoofstepSound extends Video.Frame.Sound
        {
            public void Play()
            {
                
            }
            
            public void SaveToXml(CaseXmlWriter writer) throws IOException
            {
                writer.StartElement("HoofstepSound");
                writer.EndElement();
            }
        }
        
        private class SpecifiedSound extends Video.Frame.Sound
        {
            private String sfxId;
            
            public SpecifiedSound(String sfxId)
            {
                this.sfxId = sfxId;
            }
            
            public void Play()
            {
                
            }
            
            public void SaveToXml(CaseXmlWriter writer) throws IOException
            {
                writer.StartElement("SpecifiedSound");
                writer.WriteTextElement("SfxId", this.sfxId);
                writer.EndElement();
            }
        }
    }
}
